iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
JavaScript

JavaScript為什麼筆記本系列 第 9

Day 09|Vite 啟動之後,瀏覽器到底怎麼找到 App.jsx?

  • 分享至 

  • xImage
  •  

前面八天,我們一直在追這行xd

npm run dev

從 npm、package.jsonnode_modules/.bin,一路追到 Vite。

到 Day 8,我們已經走到:

npm run dev
↓
Vite 啟動開發伺服器
↓
localhost:5173
↓
瀏覽器看到網頁

我們平常改的是:

src/App.jsx

存檔之後,瀏覽器就跟著變。

問題是:

瀏覽器到底怎麼知道要去找 App.jsx

今天就沿著真的檔案走一次。


打開我們前面的 vite-why-lab

Day 1 建立的還是同一個專案:

cd vite-why-lab
npm run dev

如果前面的 vite-why-lab 還在,就直接打開它。

沒有的話,手邊任何一個 Vite + React 專案也可以,我們今天只是想看看:這些檔案到底是怎麼串起來的。

專案裡大概會看到:

vite-why-lab/
│
├─ index.html
├─ package.json
├─ vite.config.js
├─ node_modules/
│
└─ src/
   ├─ main.jsx
   ├─ App.jsx
   ├─ App.css
   ├─ index.css
   └─ assets/

srcsource 的縮寫,可以先把它理解成:

專案主要原始碼放的地方。

今天先追三個檔案:

index.html
src/main.jsx
src/App.jsx

先打開真的 App.jsx

打開:

src/App.jsx

會看到比想像中多很多的東西。

以我現在這個 Vite React 專案為例,開頭大概是:

import { useState } from 'react'
import heroImg from './assets/hero.png'
import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      {/* 中間還有一大串預設畫面 */}
    </>
  )
}

export default App

下面還有圖片、按鈕、計數器、Documentation 連結等等。

但今天先抓住最後這句:

export default App

先把它放在心裡,等等會看到它的另一半。

順便留一個小問號:

import heroImg from './assets/hero.png'
import './App.css'

咦,怎麼連圖片和 CSS 都可以 import

先知道一件事就好:Vite 不只是幫我們開 localhost,它也在處理這些模組。


瀏覽器一開始拿到的是 index.html

回到專案根目錄,我現在這個 vite-why-lab/index.html 實際長這樣:

<!doctype html>

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>vite-why-lab</title>
  </head>

  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

以前看到這一整份,大概只知道「這是 HTML」。

現在可以把它拆成兩大塊:

<head>
→ 告訴瀏覽器這個網頁的一些基本設定

<body>
→ 真正放頁面內容,以及 React 的入口

先看 head

<meta charset="UTF-8" />

告訴瀏覽器文字編碼使用 UTF-8,中文、英文、符號才有一致的解讀方式。

<link rel="icon" type="image/svg+xml" href="/favicon.svg" />

是瀏覽器分頁旁邊的小圖示。

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

主要是在告訴手機瀏覽器:頁面寬度照裝置螢幕寬度來算。

<title>vite-why-lab</title>

就是瀏覽器分頁上看到的標題。

真正跟 React 接起來的,是在 body 裡面的這兩行:

<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>

<div id="root"></div> 一開始空空的

瀏覽器解析 HTML 時,會把 HTML 轉成一棵可以被 JavaScript 操作的 DOM。
莫慌,請繼續往下看:

例如我們的 body

<body>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>

可以先想成瀏覽器建立出:

document
└─ html
   ├─ head
   └─ body
      └─ div#root

所以這行:

<div id="root"></div>

本來就存在於 index.html 裡,一開始裡面什麼都沒有。

可以想像成:

HTML 先留一塊空地給 React。

等等的 main.jsx (你可以現在看一下這個專案的結構,旁邊有一個main.jsx) 裡會出現:

document.getElementById('root')

它就是在這棵 DOM 裡問:

幫我找到 id 叫做 root 的元素。

答案就是剛才這個:

<div id="root"></div>

下一行才把我們帶到 main.jsx

root 後面緊接著:

<script type="module" src="/src/main.jsx"></script>

這行可以先拆成三塊。

第一個是:

<script>

意思是這裡要載入、執行 JavaScript。

第二個是:

src="/src/main.jsx"

意思是:

JavaScript 的入口在 /src/main.jsx

所以 index.html 完全沒有直接寫:

App.jsx

第一段路線其實是:

index.html
↓
main.jsx

不是:

index.html
↓
App.jsx

/src/main.jsx 前面的 / 是什麼?

這個 / 不是 Mac 或 Windows 硬碟的根目錄。

在這裡可以先理解成:

從目前網站的根路徑開始找。

我們現在開的是:

http://localhost:5173

所以:

/src/main.jsx

可以想成瀏覽器去請求:

http://localhost:5173/src/main.jsx

而現在站在中間接這個請求的人,就是 Vite 的開發伺服器。

所以不是瀏覽器直接跑進你的 Mac 硬碟找:

/Users/.../src/main.jsx

而是:

瀏覽器
↓
GET /src/main.jsx
↓
Vite dev server
↓
找到專案裡的 src/main.jsx
↓
處理後回給瀏覽器

type="module" 到底是在說什麼?

完整這行是:

<script type="module" src="/src/main.jsx"></script>

type="module" 是在告訴瀏覽器:

這支 JavaScript 是一個 ES Module,請用模組的方式載入它。

這件事很快就會有意義。

因為 main.jsx 裡馬上會看到:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

這些 import,以及剛才 App.jsx 裡的:

export default App

就是 JavaScript 模組系統的一部分。

所以這行

<script type="module" src="/src/main.jsx"></script>

現在可以白話讀成:

載入 /src/main.jsx,並把它當成 JavaScript module 來執行。

另外,module script 預設有類似 defer 的行為:可以先下載,但會等 HTML 文件解析完成後再執行。

而我們這份 index.html 本身也是先寫:

<div id="root"></div>

才寫:

<script type="module" src="/src/main.jsx"></script>

所以等 main.jsx 真正執行到:

document.getElementById('root')

瀏覽器通常早就已經把:

<div id="root"></div>

建立成 DOM 元素了。


可是為什麼是 main.jsx?不能直接 App.jsx 嗎?

不知道有沒有人想過這個問題XD
既然最後真正想看到的是 App.jsx 的畫面,那 index.html 為什麼不乾脆寫:

<script type="module" src="/src/App.jsx"></script>

還要多一個 main.jsx

答案其實不是「React 規定一定要有 main.jsx」。

main.jsx 也不是瀏覽器認得的特殊檔名。

真正讓它成為入口的是:

<script type="module" src="/src/main.jsx"></script>

index.html 指定:

JavaScript 從 main.jsx 開始。

假設今天真的把 main.jsx 改名成:

banana.jsx

然後 index.html 也跟著改:

<script type="module" src="/src/banana.jsx"></script>

banana.jsx 一樣可以成為入口。

所以重點不是 main 這個名字。

重點是入口檔案負責把 React 啟動起來。

現在的 main.jsx 做的是:

找到 index.html 裡的 root
↓
建立 React root
↓
把 App 拿進來
↓
把 <App /> render 進去

App.jsx 比較專心做另一件事:

描述 App 本身的畫面和元件。

所以可以先把兩個角色分成:

main.jsx
= React 怎麼啟動、要掛在哪裡

App.jsx
= App 本身長什麼樣子

如果真的讓 index.html 直接指向 App.jsx,也不是完全不行。

只是那時候 App.jsx 就得連原本 main.jsx 的工作一起做,例如:

import { createRoot } from 'react-dom/client'

function App() {
  return <h1>Hello</h1>
}

createRoot(document.getElementById('root')).render(
  <App />
)

這樣也能跑,但「怎麼啟動 React」和「App 畫什麼」就混在同一個檔案裡了。

Vite 的 React 範本幫我們拆成兩層:

index.html
↓
main.jsx      ← 啟動 React
↓
App.jsx       ← 主要 App 元件

打開 main.jsx,終於看到 App 的另一半

打開:

src/main.jsx

內容大概是:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

先只看這句:

import App from './App.jsx'

這時候就可以把剛才 App.jsx 最下面那句接起來:

App.jsx                         main.jsx

export default App   ───────→   import App from './App.jsx'

原來 main.jsx 不是自己「知道」有一個 App.jsx

是我們明明白白寫了:

import App from './App.jsx'

它才去把 App 拿進來。


./App.jsx./ 又是什麼?

剛才 index.html 裡是:

/src/main.jsx

現在變成:

./App.jsx

./ 可以先讀成:

從目前這個檔案所在的資料夾開始找。

現在 main.jsx 在:

src/main.jsx

App.jsx 也在同一層:

src/
├─ main.jsx
└─ App.jsx

所以:

import App from './App.jsx'

就是:

去旁邊的 App.jsx,把它 export 出來的 App 拿進來。


App 拿進來之後,要畫到哪裡?

現在就可以把前面的 index.html 接回來了。

index.html 先準備:

<div id="root"></div>

main.jsx 再找:

document.getElementById('root')

兩邊正好對上:

index.html

<div id="root"></div>
        ↑
        │
main.jsx

document.getElementById('root')

再往外看:

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

現在可以把它翻成人話:

main.jsx 找到 HTML 裡的 root
↓
把這塊地方交給 React
↓
把 <App /> render 進去

原本:

<div id="root"></div>

一開始是空的。

React 啟動後,可以概念性地想成:

<div id="root">
  React 畫出來的內容
</div>

所以整條路現在變成:

index.html
│
├─ 先建立 <div id="root"></div>
│
└─ 載入 /src/main.jsx
          ↓
       main.jsx
          ↓
   找到 #root
          ↓
   import App
          ↓
       App.jsx
          ↓
   render <App />
          ↓
        畫面

可是瀏覽器真的看得懂 .jsx 嗎?

看到這裡又冒出一個問題。

我們寫的是:

<App />

甚至 App.jsx 裡面直接有:

<section>
  <h1>Get started</h1>
</section>

這長得很像 HTML,但它其實寫在 JavaScript 檔案裡。

瀏覽器不是直接拿 .jsx 原始碼來執行的。

開發時,中間還站著 Vite:

瀏覽器
↓
向 Vite 要 /src/main.jsx
↓
Vite 讀取、處理 main.jsx / App.jsx
↓
把瀏覽器能執行的 JavaScript 模組送回去
↓
瀏覽器執行

所以 Day 8 的 Vite 又接回來了。

原來:

npm run dev

不是單純「幫我開一個 localhost」。

我們開發時寫的 JSX、CSS import、圖片 import,以及模組之間的關係,Vite 都在中間幫忙處理。


那正式部署到 Nginx 或 Cloudflare 呢?

這裡我又想到另一件事。

如果最後網站不是跑在:

localhost:5173

而是部署到 Nginx,或放到像 Cloudflare Pages 這類靜態託管環境,還是一樣嗎?

概念很像,但正式上線前通常會先做:

npm run build

Vite 會產生可以部署的檔案,例如:

dist/
├─ index.html
└─ assets/
   ├─ index-xxxxx.js
   └─ index-xxxxx.css

這時候正式網站的入口仍然可以是:

dist/index.html

但它已經不是再去載入:

/src/main.jsx

而是載入 Vite build 出來的 JavaScript、CSS 等檔案。

所以可以先分成兩張圖。

開發時:

index.html
↓
/src/main.jsx
↓
App.jsx
↓
Vite 即時處理
↓
瀏覽器

正式 build 後:

App.jsx / main.jsx / CSS / 圖片
↓
Vite build
↓
dist/index.html + dist/assets/*
↓
Nginx / 靜態託管
↓
瀏覽器

Nginx 不需要看懂 React。

它主要做的是把 build 好的 HTML、JavaScript、CSS 等檔案送給瀏覽器;真正執行 React 的還是瀏覽器。


今天把整條線接起來

前幾天我們追的是:

npm run dev
↓
npm
↓
package.json
↓
node_modules
↓
Vite
↓
localhost

今天接的是:

localhost
↓
index.html
↓
建立 <div id="root"></div>
↓
<script type="module" src="/src/main.jsx">
↓
main.jsx
↓
document.getElementById('root')
↓
import App from './App.jsx'
↓
App.jsx
↓
render(<App />)
↓
畫面

好累Zzzzz
希望對大家有幫助@@


上一篇
Day 08|Vite 到底做了什麼?
下一篇
Day 10|把 Header、Footer 拆成不同的 JSX
系列文
JavaScript為什麼筆記本20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言